
from PySide import QtCore, QtGui, QtXml


# We want to add some items to a combo box under the shape property of the Light Node ui. For that we have used
# enumerators. We could have used some other mechanism but this can be a good alibi for learning about enumerators.
# To see how enums are emulated in PyQt/PySide, see python snippets collection in theNewBoston's file pr001_basics.py
def enum(*enumerated):
    # For me the below two lines are equivalent by output because both create a list of numbers.
    # enums = dict(zip(enumerated, [i for i in range(0, len(enumerated))] ))
    enums = dict(zip(enumerated, range(len(enumerated))))
    # Add a new item to the dictionary with "names" as key and list of the passed "enumerated" arguments as value
    enums["names"] = enumerated
    # The type function returns the type of an object. The return value is a "type object".  The isinstance() built-in 
    # function is recommended for testing the type of an object.
    # With three arguments, return a new type object. This is essentially a dynamic form of the class statement.
    # The name string is the class name and becomes the __name__ attribute; the bases tuple itemizes the base classes 
    # and becomes the __bases__ attribute; and the dict dictionary is the namespace containing definitions for class body 
    # and becomes the __dict__ attribute.
    return type('enum', (), enums)
# Create the enumerators. We will check for the End enumerator object to see if we have reached the end of the
# enumerator list.
LIGHT_SHAPES = enum("Point", "Spot", "Direction", "Area", "Volumetric", "End")

class Node(object):
    def __init__(self, name, parent = None):
        self._name = name       # Name of the node
        self._children = []     # List of Children of the node
        self._parent = parent   # Parent of the node
        
        # If there is a parent to this node, then this node must add itself to the child list of the parent node
        if parent is not None:
            parent.addChild(self)
        
    # typeInfo method will return a string which will describe the type of this item. Since it is a node, it will return type as Node
    def typeInfo(self):
        return "Node"
    
    # Add a new child to the list of children
    def addChild(self, child):
        self._children.append(child)    # Append the new child to the list of children using append method.
        child._parent = self    # Make itself the parent of that child. If we don't do this, we may get
                                    # errors when we are dealing with drag and drop operations later in the future. 
        
    # Similar to the add child method above but puts the child at a given arbitrary location.
    def insertChild(self, position, child):
        # We check if the position is within the boundary of the list. If not, we return false.
        if position < 0 or position > len(self._children):
            return False
        # Else put the child at the given position using the insert method. Set the parent of the child to self and return true 
        self._children.insert(position, child)
        child._parent = self
        return True
    
    # Just like the insert child method, the remove child method is implemented as follows.
    def removeChild(self, position):
        # We check if the position is within the boundary of the list. If not, we return false.
        if position < 0 or position > len(self._children):
            return False
        # Else get the child at the given position using the pop method. Then et the parent of the child to None and return true 
        child = self._children.pop(position)
        child._parent = None
        return True    

    # Alternate short implementation of getter and setter methods.
    # Getters
    # def name(self):
    #    return self._name
    # Setters
    # def setName(self, value):
    #    self._name = value
    # Instead of the above getters and setters we will use the following code.
    # Here is a method to get and set the name of an object. The property function (see pop up help) has four keyword parameters
    # fget, fset, fdel and doc. We  define them in our name method. The fget and fset functions get and set an attribute.
    # The locals() function updates and returns a dictionary containing the current scope's local variables. We then take that
    # and pass it as 'keyword parameters' to the property function.
    # So now if we do, self.name = 5, it will call fset. If we do x = self.name, or return self.name, it will call fget. That
    # is a simple change in variable or reading from the "name" will call the fget and fset automatically and that is why
    # we didn't need to get and set the name with a method call like name() or getName() or setName()
    
    def name():
        def fget(self): return self._name
        def fset(self, value): self._name = value
        return locals()
    name = property(**name())
    # Getting and setting attributes this way has to do with xml serializing. We can actually have a way to extract data without
    # using a function as .. def getAttrs(): return ["name", "typeInfo"]. Now we have a single function that can return properties
    # (or attributes) and is also inherited from the super classes.
    
    # Method to get the child corresponding to an index (or row)
    def child(self, row):
        return self._children[row]
    
    # Method to get the number of children
    def childCount(self):
        return len(self._children)
    
    # Method to get the parent of this node
    def parent(self):
        return self._parent
    
    # Method to get the index number of this node relative to its parent
    def row(self):
        if self._parent is not None:
            return self._parent._children.index(self)
        
    # Method to display the data in the console
    def log(self, tabLevel = -1):
        output = ""
        tabLevel += 1
        
        for i in range(tabLevel):
            output += "\t"
        
        output += self._name + "\n"
        
        for child in self._children:
            output += child.log(tabLevel)
        
        tabLevel -= 1
        return output
    
    def __repr__(self):
        return self.log()
    # These are the methods that the node class will use to decide (ho ho) what to return and to set the value.
    # The methods are data, setData and resource.
    def data(self, column):
        # return the node name for column 0 and the node typeInfo for column 1
        if column is 0:
            # Note the we have not used a function call ( as self.name() ). The reason is around line 53 
            return self.name
        elif column is 1:
            return self.typeInfo()
    
    def setData(self, column, value):
        # Set node name to the given value if the user modifies it. The node name is in column 0.
        # But if the user tries to change the node type, i.e. column 1, then don't do anything, just pass it.
        if column is 0:
            #self.name = value.toPyObject()
            self.name = value
        elif column is 1:
            pass
    
    def resource(self):
        # For the simple node, return None for resource because we don't have an image for it. But later if we want
        # an image for it, this is the place to add the resource.
        # Never forget to put the extension of the file when working with resources compiled into binary
        return None
    
    # Calling the attrs() method on an object will return all the properties of that object like nearRange, farRange etc.
    # The return type is a dictionary containing property names as key and the attribute values as value
    def attrs(self):
        # instance.__class__:    The class to which a class instance belongs.
        # class.__mro__:  This attribute is a tuple of classes that are considered when looking for base classes during method resolution.
        # So if we write the following lines in the __main__ method, we will get the given results 
        # light = LightNode("lihghh")
        # print(light.__class__)                -->    <class 'Data.LightNode'>
        # print(light.__class__.__mro__)    -->    (<class 'Data.LightNode'>, <class 'Data.Node'>, <class 'object'>)
        # So the __mro__ gives all the classes that our object derived from.
        classes = self.__class__.__mro__    # Get all the classes
        kvPair = {}
        # For all the class names we can get a dictionary using the __dict__ attribute. If we print that dictionary we can 
        # get all the attributes that the class have. Try print(cls.__dict__) in the for loop.
        # But here we are not interested in getting all attributes. We want to the get only the values (or variables) which
        # are the "instance of property". That means we want to get the properties of the Node, CameraNode, LightNode
        # etc. which are motionBlur, shakeIntensity, nearRange, farRange etc.
        for cls in classes:
            # items() Vs iteritems()
            # Originally Python items() built a real list of tuples and returned that. That could potentially take a lot of extra memory. 
            # Then generators were introduced to the language in general, and that method was reimplemented as a iterator-generator method 
            # named iteritems(). The original being left for backwards compatibility. One of the changes of Python 3.x is that the items() 
            # methods now also return iterators and a list is never fully built. In Python 3.x there is no iteritems() method any longer, 
            # as items() is now the same as iteritems() in Python 2.x.
            # The .items() function gives you a list of tuples from dictionary items. When you loop over them using two
            # variables (as below), each tuple is unpacked into a key and value automatically 
            for key, value in cls.__dict__.items():
                if isinstance(value, property):
                    # Try the following line to print the properties of the calling class
                    # print("Property Name: ", key)
                    # The string.rstrip(<any char>) or string.rstrip() returns a string with the given characters removed
                    # from the right side.
                    print("Property: ", key.rstrip("_"), "\n\tValue", value.fget(self))
                    kvPair[key] = value.fget(self)
        return kvPair
    
    
    # Methods to generate the Xml code of the node hierarchy of our data model. The best method would probably be to 
    # put them in separate classes named NodeXmlWriter and NodeXmlReader, but we leave that for now.
    # Ee have put the updateXml method in our controllers class. In that method we just called the rootNode.asXml()
    # to get the Xml code of the whole hierarchy. We then used the returned Xml and put it in the uiXml QTextEdit box
    # which is in the Window.ui file. Then we applied Xml highlighting on that ui to get the highlighted Xml result.
    def asXml(self):
        # This is how we use the Qt Xml package. First we need to create a Qt Dom Document. Think of that as your virtual 
        # Xml file.
        doc = QtXml.QDomDocument()
        # Then we can call some factor methods of that document (doc) like createElement, createAttribute, createNode
        # etc. We create an element using the typeInfo and we return it to node. This creates something like this <Node/>
        # i.e. and empty node in the Xml file.
        node = doc.createElement(self.typeInfo())
        # And then we append it to that dom document.
        doc.appendChild(node)
        # The we start looping on the children of this node and call the recurseXml method and we pass the document and
        # the node to it. 
        for i in self._children:
            i._recurseXml(doc, node)
        return doc.toString(indent=4)
    
    # The recurseXml takes a doc object (i.e. a DomDocument) and a parentDom as its parameters. 
    def _recurseXml(self, doc, parentDom):
        # Creates another element, appends it to the parent.
        node = doc.createElement(self.typeInfo())
        parentDom.appendChild(node)
        # Call the attributes method which return every attribute in the form of a dictionary.
        attrs = self.attrs().items() 
        # Then for every key (name of the attibute) and value (value of the attribute), we call a method called setAttribute
        # on the node we created by calling the createElement
        for key, value in attrs:
            node.setAttribute(key, value)
        # And then we recurse the children of this child by calling recurseXml
        for i in self._children:
            i._recurseXml(doc, node)
    
    
# Let's make some subclasses from the Node class and call them LightNode, TransformNode, CameraNode. All of them would need to 
# reimplement the typeInfo method. Initially we forced each class to have a parent. But now we changed it be without a parent.
# ** We also have modified these nodes to include their attributes as protected members.
# ** Also implemented are the getters and setters for those properties.

class TransformNode(Node):
    def __init__(self, name, parent = None):
        # Call the super class constructor i.e. the Node class. 
        super(TransformNode, self).__init__(name, parent)

        self._x = 0.0
        self._y = 0.0
        self._z = 0.0
        
    # Re-implement the typeInfo method
    def typeInfo(self):
        return "TransformNode"
    
    def x():
        def fget(self): return self._x
        def fset(self, value): self._x = value
        return locals()
    x = property(**x())

    def y():
        def fget(self): return self._y
        def fset(self, value): self._y = value
        return locals()
    y = property(**y())
    
    def z():
        def fget(self): return self._z
        def fset(self, value): self._z = value
        return locals()
    z = property(**z())


    def data(self, column):
        # Calling the super class (the Node class) data method. We have called that method from the super class so that
        # we don't need to return write the code for returning name or typeInfo again. Rather we called the desired
        # method from the super class.
        r = super(TransformNode, self).data(column) 
        
        if column is 2:
            r = self.x
        elif column is 3:
            r = self.y
        elif column is 4:
            r = self.z
        
        return r
    
    def setData(self, column, value):
        # Call the super class' setData method
        super(TransformNode, self).setData(column, value)
        
        if column is 2:
            # self.x = value.toPyObject()
            self.x = value
        elif column is 3:
            # self.y = value.toPyObject()
            self.y = value
        elif column is 4:
            # self.z = value.toPyObject()
            self.z = value
            
    def resource(self):
        # Never forget to put the extension of the file when working with resources compiled into binary
        return ":/Transform.png"

class CameraNode(Node):
    def __init__(self, name, parent = None):
        # Call the super class constructor
        super(CameraNode, self).__init__(name, parent)
        
        self._motionBlur = True
        self._shakeIntensity = 50.0
        
    # Reimplement the typeInfo method
    def typeInfo(self):
        return "CameraNode"
    
    # Getter and setter methods
    def motionBlur():
        def fget(self): return self._motionBlur
        def fset(self, value): self._motionBlur = value
        return locals()
    motionBlur = property(**motionBlur())

    def shakeIntensity():
        def fget(self): return self._shakeIntensity
        def fset(self, value): self._shakeIntensity = value
        return locals()
    shakeIntensity = property(**shakeIntensity())

        
    def data(self, column):
        # Calling the super class (the Node class) data method. It either returns name or typeInfo
        r = super(CameraNode, self).data(column) 
        
        if column is 2:
            r = self.motionBlur
        elif column is 3:
            r = self.shakeIntensity

        return r
    
    def setData(self, column, value):
        super(CameraNode, self).setData(column, value)
        
        if column is 2:
            # self.motionBlur = value.toPyObject()
            self.motionBlur = value
        if column is 3:
            # self.shakeIntensity = value.toPyObject()
            self.shakeIntensity = value
            
    def resource(self):
        # Never forget to put the extension of the file when working with resources compiled into binary
        return ":/Camera.png"



class LightNode(Node):
    def __init__(self, name, parent = None):
        # Call the super class constructor
        super(LightNode, self).__init__(name, parent)
        
        self._intensity = 1.0
        self._nearRange = 40.0
        self._farRange = 80.0
        self._castShadows = True
        # By default, the light editor ui will show the first element of the enumerator list (i.e. "Point")
        self._shape = LIGHT_SHAPES.names[0]
        
    # Re-implement the typeInfo method
    def typeInfo(self):
        return "LightNode"
    
    # Getter and setter methods
    def intensity():
        def fget(self): return self._intensity
        def fset(self, value): self._intensity = value
        return locals()
    intensity = property(**intensity())

    def nearRange():
        def fget(self): return self._nearRange
        def fset(self, value): self._nearRange = value
        return locals()
    nearRange = property(**nearRange())

    def farRange():
        def fget(self): return self._farRange
        def fset(self, value): self._farRange = value
        return locals()
    farRange = property(**farRange())

    def castShadows():
        def fget(self): return self._castShadows
        def fset(self, value): self._castShadows = value
        return locals()
    castShadows = property(**castShadows())
    
    def shape():
        def fget(self): return self._shape
        def fset(self, value): self._shape = value
        return locals()
    shape = property(**shape())
    

    def data(self, column):
        # Calling the super class (the Node class) data method. It either returns name or typeInfo
        r = super(LightNode, self).data(column) 
        
        if column is 2:
            r = self.intensity
        elif column is 3:
            r = self.nearRange
        elif column is 4:
            r = self.farRange
        elif column is 5:
            r = self.castShadows
        elif column is 6:
            # The returned value will be a string. We need an integer so that the combo box could operate on it.
            # So we have the enumerator, having a member called names which is a list so we ask for the index of the 
            # current shape which would be an integer.
            r = LIGHT_SHAPES.names.index(self.shape)
        return r
    
    def setData(self, column, value):
        super(LightNode, self).setData(column, value)
        
        if column is 2:
            # self.intensity = value.toPyObject()
            self.intensity = value
        elif column is 3:
            # self.nearRange = value.toPyObject()
            self.nearRange = value
        elif column is 4:
            # self.farRange = value.toPyObject()
            self.farRange = value
        elif column is 5:
            # self.castShadows = value.toPyObject()
            self.castShadows = value
        elif column is 6:
            # We made some modification in the data method of the light node class to return an integer so that the
            # combo box could operate. But this would be a problem for the setData method since it tries to set the
            # data (shape) as an integer too.
            # Using LIGHT_SHAPES.names[value] will return the string name which we will use to set the data. 
            self.shape = LIGHT_SHAPES.names[value]
            
    def resource(self):
        # Never forget to put the extension of the file when working with resources compiled into binary
        return ":/Light.png"
